package com.ost.kautilya.modeling.weka;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.Serializable;
import java.util.logging.Logger;

import weka.classifiers.Classifier;
import weka.classifiers.Evaluation;
import weka.classifiers.evaluation.output.prediction.PlainText;
import weka.core.Attribute;
import weka.core.Capabilities;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.OptionHandler;

import com.ost.kautilya.modeling.DataRow;
import com.ost.kautilya.modeling.ModelBinary;

public class WekaClassifierModel extends WekaModel {
	private final static Logger logger = Logger.getLogger(WekaClassifierModel.class.getName());

	private static final long serialVersionUID = 1L;
	public static final String CLASSIFIER_BINARY = "classifier";

	public WekaClassifierModel(Class<?> cfc) throws Exception {
		this(cfc, ModelType.CLASSIFICATION);
	}

	protected WekaClassifierModel(Class<?> cfc, ModelType type) throws Exception {
		super(type);
		getBinary().put(CLASSIFIER_BINARY, (Serializable) cfc.newInstance());
		setParameter(1, cfc.getName());
	}

	public Object[] predictFor(Instance in) throws Exception {
		// System.out.println("Predict for - " + in);
		Object[] value = new Object[getClasses().length];
		Classifier clf = getClassifier();
		double val = 0;
		synchronized (clf) {
			val = clf.classifyInstance(in);
		}
		for (int cli = 0; cli < getClasses().length; cli++) {
			Attribute cla = getWekaAttributes().get(cli);
			switch (cla.type()) {
			case Attribute.NOMINAL:
				value[cli] = ((Attribute) getWekaAttributes().get(getClassIndexInAttributes())).value((int) val);
				break;
			case Attribute.NUMERIC:
				value[cli] = val;
				break;
			}
		}
		// System.out.println("Predicted value - " + Arrays.toString(value));
		return value;
	}

	@Override
	public void applyConfiguration(String[] options) throws Exception {
		Classifier clf = getClassifier();
		if (clf instanceof OptionHandler) {
			((OptionHandler) clf).setOptions(options);
		}
	}

	@Override
	public void finalizeTraining(DataRow tr) throws Exception {
		// String[] opts = ((OptionHandler) getClassifier()).getOptions();
		// for (String opt : opts) {
		// System.out.println(opt);
		// }
		if (splitMode) {
			trainWithRecordIncrementally(tr);
		}
		Instances tins = super.getTrainingInstances();
		// System.out.println(tins.size() + " - " + tins.relationName());
		// System.out.println(tins);
		getClassifier().buildClassifier(tins);
	}

	@Override
	public void finalizeEvaluation(DataRow tr) throws Exception {
		if (splitMode) {
			evaluateRecordIncrementally(tr);
		}
		Classifier clf = getClassifier();
		Instances tins = getTrainingInstances();
		Evaluation ev = new Evaluation(tins);
		Instances vins = getEvaluationInstances();
		byte[] output = null;

		File tmpout = null;
		PlainText html = new PlainText();
		try {
			tmpout = new File(System.getProperty("java.io.tmpdir"), getModelId() + ".evalout");
			if (!tmpout.exists()) {
				tmpout.createNewFile();
			}
			html.setOutputFile(tmpout);
		} catch (Exception e) {
			logger.warning("Failed to create model evaluation output file " + tmpout + ". Cause [" + e.getClass().getSimpleName() + "] - " + e.getMessage());
		}
		html.setBuffer(new StringBuffer());
		html.setHeader(vins);
		html.setNumDecimals(3);
		html.setSuppressOutput(false);
		ev.evaluateModel(clf, vins, html);
		if (tmpout != null && tmpout.exists()) {
			InputStream in = null;
			try {
				in = new FileInputStream(tmpout);
				output = new byte[in.available()];
				in.read(output, 0, output.length);
			} catch (Exception e) {
				logger.warning("Failed to read model evaluation output file " + tmpout + ". Cause [" + e.getClass().getSimpleName() + "] - " + e.getMessage());
			} finally {
				if (in != null) {
					in.close();
				}
				if (!tmpout.delete()) {
					tmpout.deleteOnExit();
				}
			}
		}
		ByteArrayOutputStream bout = null;
		try {
			bout = new ByteArrayOutputStream();
			bout.write(output);
			bout.write(ev.toSummaryString(true).getBytes());
			output = bout.toByteArray();
		} catch (Exception e) {
			logger.warning("Failed to add summary to model evaluation output. Cause [" + e.getClass().getSimpleName() + "] - " + e.getMessage());
		}
		addEvaluation(tr, new WekaClassifierEvaluation(ev, getClasses().length, output));
	}

	@Override
	public String getDescription() {
		return getClassifier().getClass().getSimpleName();
	}

	@Override
	public Capabilities getCapabilities() {
		return getClassifier().getCapabilities();
	}

	protected Classifier getClassifier() {
		return (Classifier) getBinary().get(CLASSIFIER_BINARY);
	}

	@Override
	public ModelBinary generateBinary() throws Exception {
		ModelBinary bin = super.generateBinary();
		if (getParameter(1) != null)
			bin.put(CLASSIFIER_BINARY, (Serializable) Class.forName(getParameter(1)).newInstance());
		return bin;
	}

	@Override
	public double[] distributionFor(Instance in) throws Exception {
		double val = 0;
		double[] dist = new double[getClasses().length];
		double[] value = new double[getClasses().length];
		try {
			Classifier clf = getClassifier();
			synchronized (clf) {
				val = clf.classifyInstance(in);
				dist = clf.distributionForInstance(in);
			}
			for (int cli = 0; cli < getClasses().length; cli++) {
				Attribute cla = getWekaAttributes().get(cli);
				switch (cla.type()) {
				case Attribute.NOMINAL:
					value[cli] = dist[((int) val)];
					break;
				case Attribute.NUMERIC:
					value[cli] = 1;
					break;
				}
			}
		} catch (Exception e) {
		}
		return value;
	}

}
